Android's publish-subscribe backbone. A system-wide event bus where the kernel, framework, and apps broadcast signals — and any authorized listener can react, regardless of whether it's running. Understand the design from the inside out.
Android is a multi-process, multi-application OS. At any moment, dozens of apps and system services exist in separate Linux processes, with strict UID-level isolation. They cannot call each other's methods, read each other's memory, or subscribe to each other's events directly.
Yet real devices generate a constant stream of events — power connected, network changed, SMS received, time zone updated, screen turned off. Every app that cares needs to know. Without a system-level pub/sub mechanism, each app would need to poll indefinitely — burning CPU, draining battery, and producing race conditions.
"Broadcast Receiver is the OS saying: I have an event. I don't care who's listening, or whether they're running. If you registered interest, you'll hear it — and you'll hear it exactly once."
Hardware and OS state changes — battery low, boot completed, airplane mode toggled — need to reach all interested apps without the OS knowing who they are in advance.
Apps need loose coupling. Sending an Intent broadcast lets an app signal an event without knowing — or caring — which other apps handle it.
The receiver doesn't need to be running. The system can start the receiver's process on demand — making it possible to react to events even after a force-stop.
Android has evolved the broadcast system across many API versions. Today there are four distinct types, each with different delivery guarantees, ordering, and security implications.
Beyond type, broadcasts are also either implicit (addressed to an action string, any matching receiver can respond) or explicit (addressed directly to a component by class name or package). Since API 26, most implicit broadcasts cannot be received by manifest-registered receivers — apps must use runtime-registered receivers or explicit broadcasts instead.
System broadcasts (like android.intent.action.BOOT_COMPLETED) are sent by the OS with signature-level or special permissions. App broadcasts are custom actions defined by developers. System broadcasts are protected — third-party apps cannot spoof them because sendBroadcast() from a non-system process using a protected action is rejected by ActivityManagerService.
When your app calls sendBroadcast(intent), that call enters a deep pipeline involving ActivityManagerService, Binder IPC, PackageManager lookups, and process lifecycle management. Here's every step.
Your call goes through ContextImpl.sendBroadcast(), which immediately delegates via Binder IPC to ActivityManagerService (AMS) running in the system_server process. Your process is unblocked immediately — the call is fire-and-forget. AMS handles everything from here.
AMS calls PackageManagerService.queryBroadcastReceivers(intent) to find all manifest-registered receivers matching the Intent's action, category, and data. It also checks its in-memory list of runtime-registered receivers (from registerReceiver() calls). Permission filters and background restrictions are applied here.
AMS has two BroadcastQueues: a "foreground" queue (for broadcasts with Intent.FLAG_RECEIVER_FOREGROUND) and a "background" queue. The broadcast record is enqueued to the appropriate queue. Normal broadcasts go background; system critical ones go foreground with lower latency budget.
Foreground broadcasts must be processed within ~10 seconds per receiver. Background broadcasts have a 60-second timeout. If your onReceive() blocks for too long, AMS declares an ANR — even for background receivers.
For manifest-registered receivers, AMS checks if the receiver's process is alive. If not, it calls startProcessLocked() — forking from Zygote, initializing the Application class, and then delivering the broadcast. This means a broadcast can cold-start a dead app in milliseconds just to run your 10-line onReceive().
AMS delivers the Intent via Binder to the receiver's process. The ActivityThread (which runs your app's main thread message loop) receives the RECEIVER message and invokes BroadcastReceiver.onReceive(context, intent) synchronously on the main thread. Once this method returns, AMS considers the receiver done — and can kill the process.
@Deprecated("Use WorkManager or JobScheduler for background work") class MyReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { // onReceive() runs on main thread. DO NOT block here. // If you need async work: use goAsync() to extend the window. val pendingResult = goAsync() // tells AMS "not done yet" CoroutineScope(Dispatchers.IO).launch { try { // Do work on a background thread — still within ~10s budget processIntentData(intent) } finally { pendingResult.finish() // MUST call — releases AMS hold } } } } // MANIFEST registration (static) — survives app death <receiver android:name=".MyReceiver" android:exported="false"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"/> </intent-filter> </receiver> // RUNTIME registration (dynamic) — tied to component lifecycle val filter = IntentFilter(Intent.ACTION_BATTERY_LOW) val receiver = MyReceiver() ContextCompat.registerReceiver( context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED ) // Always unregister to prevent leaks: unregisterReceiver(receiver)
Ordered broadcasts implement the Chain of Responsibility pattern at the OS level. AMS dispatches the Intent to receivers one at a time, sorted by android:priority (range: -1000 to 1000, default 0).
Each receiver can:
getResultData()setResultData() — passed to the next receiverabortBroadcast() — downstream receivers never see itThis is exactly how SMS apps intercept messages: the SMS provider sends an ordered broadcast; your SMS app registers at high priority (999), reads the PDU, possibly shows a custom notification, and aborts so the default handler doesn't also fire.
// SENDER: ordered broadcast with initial result sendOrderedBroadcast( intent, "com.example.RECEIVE_PERMISSION", finalReceiver, // called last always null, // handler Activity.RESULT_OK, "initial data", null ) // RECEIVER: high-priority interceptor class SmsInterceptor : BroadcastReceiver() { override fun onReceive(ctx: Context, intent: Intent) { val prev = resultData // from higher-priority receiver if (shouldBlock(intent)) { abortBroadcast() // ← kills the chain here return } // Modify and pass down setResultData("modified: $prev") } } <!-- Manifest: priority 999 = very early in chain --> <receiver android:name=".SmsInterceptor"> <intent-filter android:priority="999"> <action android:name="android.provider.Telephony.SMS_RECEIVED"/> </intent-filter> </receiver>
BroadcastReceiver's lifecycle is unique — it begins when onReceive() is called and ends when it returns. That's it. This short window has profound implications for what you can and cannot do.
AMS sets a watchdog timer per receiver. Exceed 10 seconds on the main thread and the system force-kills your process with an ANR. There are no exceptions. This is kernel-level enforcement, not a suggestion.
onReceive() is always called on the main thread, regardless of which thread called sendBroadcast(). You cannot bind a service, do network I/O, or block. Even room queries block the main thread if called here.
Call goAsync() to get a PendingResult token. This extends the ANR window slightly and lets you hand off work to another thread — but you must call pendingResult.finish() when done or the process hangs.
Android apps have a single-threaded event loop running on the main thread, implemented as a Looper/Handler message queue inside ActivityThread. When AMS sends a broadcast to your process via Binder, the Binder thread posts a RECEIVER message to this queue. The main thread's Looper dequeues it and calls your onReceive(). This is the same queue that handles touch events, frame renders, and Activity lifecycle — which is exactly why blocking it for more than a few milliseconds causes visible jank, and blocking for 10+ seconds triggers ANR.
| Aspect | Static (Manifest) | Dynamic (Runtime) |
|---|---|---|
| Survives app death | ✓ Yes — system starts process | ✗ No — dies with component |
| Implicit broadcasts (API 26+) | ✗ Mostly blocked | ✓ Allowed |
| Active only when app running | ✗ Always active | ✓ Controlled lifecycle |
| Battery/memory cost | ⚠ Higher — wakes process | Lower — already running |
| Can receive in background | ✓ Yes (exceptions apply) | ✗ Only if process alive |
| BOOT_COMPLETED | ✓ Required | ✗ Not possible |
Broadcasts cross process boundaries — which makes them a potential attack surface. Android applies layered permission enforcement on both the sender and receiver sides. Understanding both is critical to writing secure apps.
Pass a permission string as the second argument to sendBroadcast(intent, permission). Only receivers that have declared that permission in their manifest will receive the broadcast. Prevents eavesdropping by unprivileged apps.
Declare android:permission on your <receiver> tag. Only senders holding that permission can deliver to you. Prevents spoofing — an untrusted app cannot impersonate the system by sending a broadcast to your protected receiver.
Required since API 33. Receivers with intent-filter must explicitly declare exported. Setting it to false means only your own app (or apps signed with the same certificate) can send broadcasts to this receiver — the system won't route any external intent to it.
Actions like android.intent.action.BOOT_COMPLETED are declared in the platform manifest with protectionLevel="signature". Only the system (UID 1000) can send them. AMS verifies the caller's UID before allowing protected action broadcasts — non-system processes that try to send them get a SecurityException.
Since Oreo, most implicit broadcasts cannot be received by manifest-registered receivers. Apps in the background cannot start Activities from broadcasts. These restrictions are enforced by AMS during dispatch — matching receivers in stopped packages are skipped entirely unless the broadcast carries FLAG_INCLUDE_STOPPED_PACKAGES.
// ✓ CORRECT: Send with permission + explicit package val intent = Intent("com.example.MY_ACTION").apply { setPackage("com.trusted.app") // explicit = no broadcast hijack addFlags(Intent.FLAG_RECEIVER_FOREGROUND) } sendBroadcast(intent, "com.example.RECEIVE_PERMISSION") // ✓ CORRECT: Receive securely — require sender permission ContextCompat.registerReceiver( context, myReceiver, IntentFilter("com.example.MY_ACTION"), "com.example.SEND_PERMISSION", // sender must hold this null, ContextCompat.RECEIVER_NOT_EXPORTED // not visible to other apps ) // ✗ WRONG: No permission, no package — any app can send/receive sendBroadcast(Intent("com.example.MY_ACTION")) registerReceiver(myReceiver, IntentFilter("com.example.MY_ACTION"))
"Since API 26, Broadcasts have become a last resort for background work. The right question is not 'how do I use a BroadcastReceiver?' but 'do I actually need one?'"
The sender of a broadcast doesn't know who receives it. The system bridges the gap — making it possible for third-party apps to react to events that the original developer never anticipated. This is the Open-Closed Principle applied to an entire OS.
The alternative to broadcasts is polling — apps waking periodically to check if something changed. Broadcast push delivery is orders of magnitude more efficient: the receiver's process only runs when there's actual work to do, then dies immediately. The 10-second hard limit enforces this discipline.
Using Intent as the message format was a deliberate unification. The same data structure that launches Activities and starts Services also carries broadcast data. Any Intent extra, action string, or URI that works in one context works in another — the broadcast system gets type-checked, extensible messaging for free.
What they gained: A universal, decoupled, process-agnostic pub/sub system that works across the entire OS — from kernel-level power events to app-defined custom actions. No shared memory, no direct coupling, no polling.
What it cost them: Uncontrolled broadcast receivers could wake hundreds of processes simultaneously (broadcast storms). Every major Android version since Lollipop has progressively restricted what manifest receivers can do — culminating in Oreo's API 26 restrictions that killed most implicit background receivers.
The lesson: The original design was too permissive. The flexibility that made broadcasts powerful also made them a battery drain vector. The API 26 restrictions are Android admitting that background wakeups must be gated — but rather than removing broadcasts, they refined them, leaving the mechanism intact while removing the most abusive usage patterns.
The fact that Broadcasts still exist and are still the only correct tool for system events is a testament to the soundness of the original design: the problem was implementation abuse, not the model itself.